iT邦幫忙

2023 iThome 鐵人賽

DAY 14
1
Cloud Native

時光之鏡:透視過去、現在與未來的 Observability系列 第 14

Loki — 解開日誌空間與時間束縛的法杖

  • 分享至 

  • xImage
  •  

Loki

資訊處理流程 生成 收集 儲存 使用
Docker Loki Driver
Loki

Log 首重使用,作為 Log 工具的第一篇,就先來介紹 Loki。Loki 是 Grafana Labs 於 2018 年開源的 Log Aggregation System,主要特點是輕量、易用、成本低、高可用且易於擴展。Loki 借鑑了 Prometheus 的設計理念,對時間和 Label 進行索引,以提高查詢效率,並推出了一種類似 PromQL 的查詢語言 LogQL。

Loki 的主要工作是接收 Log,然後將這些 Log 儲存到 Object Storage 後,最後提供 API 以便 Grafana 進行查詢。目前,已有多種 Log 收集工具提供對 Loki 的支援,不僅有 Grafana Labs 專為 Loki 設計的 Promtail,還有我們之後將介紹的 Fluent BitVector。此外,官方還開發了一個專門用於收集 Docker Container Log 的 Loki Docker Driver,使用者無需安裝額外的 Agent。在本篇 Lab 中,我們將使用 Loki Docker Driver 來收集 Docker Container 的 Log。

Pipeline

Concepts

Label

Loki 參考了 Prometheus 的設計,使用 Label 來標記 Log,方便使用者利用 Label Selector 進行查詢。Label 是由 Key-Value Pair 組成的,例如:app=nginxlevel=INFOhost=web01等。收集 Log 的工具會負責添加這些 Label,如 Loki Docker Driver 會自動添加 compose_servicecontainer_namecompose_projectfilename 等 Label。如果使用其他的 Log 收集工具,例如:Promtail、Fluent Bit、Vector 等,則需要在相應的工具中進行設定。

Label Name
Label Value
在 Grafana 查詢 Loki Data Source 時可以看到有哪些 Label 與 Label 的值

Log Detail
展開 Log 後可以看到該筆 Log 的 Label,顯示為 Field

Loki 之所以能夠快速查詢,是因為有使用 Label 進行索引。但如果 Label 設計不當,則會導致 Loki 效能下降。為此,官方文件 Label best practices 中說明該如何設計要標上哪些 Label。主要建議是不要使用會隨著 Log 內容變化的值作為 Label,例如 User ID、Trace ID、HTTP Status Code 等,因為這些值的分布範圍較廣,使 Loki 的索引變得過大,進而影響其效能。對於動態值,建議配合稍後將介紹的 Log Parser,只在查詢時動態生成新的 Label 供使用,而不是在儲存時就生成 Label。

LogQL

LogQL 是 Loki 的查詢語言,其語法與 PromQL 相似。官方網站提供了 LogQL Analyzer,可以練習 LogQL 的語法並理解每個執行步驟。除了文字內容篩選外,若要搭配其他的 Parser、Function 使用時,需要使用 |(Pipe)進行串連,就如同在使用 Linux 的 Pipe 一樣。

Log Selector

首先,我們介紹最基礎的 Label Selector。在使用 LogQL 進行查詢時,至少需要選擇一個 Label。其語法與 PromQL 相同,使用 {} 進行包裹,並利用 = 進行比較。以下是LogQL 的 Label Selector 語法:

{<label_name>=<label_value>, ...}

例如,若要查詢帶有 job Label 且其值為 analyze 的 Log,可使用:

{job="analyze"}

若需要篩選多個 Label,可使用 , 串連,例如,查詢 jobanalyzeserviceloki 的Log:

{job="analyze", servcie="loki"}

= 外,還有其他比較符號可用:

  1. !=: 不等於
  2. =~: 符合指定的正規表示式
  3. !~: 不符合指定的正規表示式

以下是正規表示式的應用範例:

  1. {name =~ "mysql.+"}: namemysql 開頭
  2. {name !~ "mysql.+"}: name 不為 mysql 開頭
  3. {name !~ `mysql-\d+`}: name 不為 mysql-1mysql-2mysql-3

文字內容篩選

Log 查詢的基礎使用情境就是文字內容篩選。LogQL 提供四種方式來篩選文字內容:

  1. |=: 包含指定文字
  2. !=: 不包含指定文字
  3. |~: 符合指定的正規表示式
  4. !~: 不符合指定的正規表示式

使用的方式如下:

{job="analyze"} |= "Logs" # 包含 `Logs` 字串的 Log
{job="analyze"} != "Logs" # 不包含 `Logs` 字串的 Log
{job="analyze"} |~ "info|error" # 同時包含 `info` 或 `error` 字串的 Log
{job="analyze"} !~ "info|error" # 同時不包含 `info` 或 `error` 字串的 Log

篩選後就只能看到被篩選的 Log,但在實務情境中,我們查詢 Log 時的篩選都只是輔助我們定位問題,定位後還會需要查看該筆 Log 的上下文,以便進一步分析問題。在 Grafana Explore 中,可以透過 Show Context 的功能,展開一個新的視窗,並顯示該筆 Log 前後的 Log。在目前最新版的 Grafana 10 針對 Loki 的查詢體驗又優化了許多功能,可以參考官方介紹影片 Loki Log Context Query Editor in Grafana 10

Show Context
Hover Log 後右側會有 Show Context 按鈕

Context Modal
顯示 Context 的視窗,也可以點擊右下的 Open in split view 用更大的畫面檢視

Log Parser

為了使 Log 更易於閱讀,目前的 Log 多以半結構化(Log Pattern)或結構化(JSON、logfmt)方式輸出。LogQL 提供了 Log Parser,讓使用者能將 Log 解析成結構化資料。解析後的 Log 會生成新的 Label,方便用於快速篩選。Log Parser 主要有四種:

  1. logfmt: 解析 logfmt 格式的 Log
  2. json: 解析 JSON 格式的 Log
  3. regexp: 使用正規表示式解析 Log
  4. pattern: 用比對文字的方式解析 Log

Label 可以再使用 | <label_name> = <value> 篩選,以 logfmt 為例,如前所述要透過 | 串連,使用的方式如下:

# 原始 Log 內容
level=info ts=2022-03-23T11:55:29.846163306Z caller=main.go:112 msg="Starting Grafana Enterprise Logs"
level=debug ts=2022-03-23T11:55:29.846226372Z caller=main.go:113 version=v1.3.0 branch=HEAD Revision=e071a811 LokiVersion=v2.4.2 LokiRevision=525040a3
level=info ts=2022-03-23T11:55:45.221254326Z caller=loki.go:355 msg="Loki started"
# LogQL,將 Log 以 logfmt 解析生成新的 Label,並篩選 level 為 info 的 Log
{job="analyze"} | logfmt | level = "info"
# 篩選後的 Log 內容
level=info ts=2022-03-23T11:55:29.846163306Z caller=main.go:112 msg="Starting Grafana Enterprise Logs"
level=info ts=2022-03-23T11:55:45.221254326Z caller=loki.go:355 msg="Loki started"

若覺得原始 Log 格式不易閱讀,可使用 line_format,依解析後的 Label 重新定義 Log 格式,語法為 | line_format "{{.label_name}}",其中雙大括號內的 label_name 會被替換為相對應的 Label 值。例如,將 Log 格式調整為只顯示 levelmsg

# LogQL
{job="analyze"} | logfmt | line_format "Level:{{.level}} Log:{{.msg}}"
# 篩選後的 Log 內容
Level:info Log:"Starting Grafana Enterprise Logs"
Level:debug Log:
Level:info Log:"Loki started"
Pattern

Pattern 是另一個好用的 Parser,可以將 Log 以比對文字的方式解析,並生成新的 Label。Pattern 的語法與正規表示式相似,但更為簡單,使用時一樣用 | 串連,例如:

# 原始 Log 內容
238.46.18.83 - - [09/Jun/2022:14:13:44 -0700] "PUT /target/next-generation HTTP/2.0" 404 19042
16.97.233.22 - - [09/Jun/2022:14:13:44 -0700] "DELETE /extensible/functionalities HTTP/1.0" 200 27913
46.201.144.32 - - [09/Jun/2022:14:13:44 -0700] "PUT /e-enable/enable HTTP/2.0" 504 26885
# LogQL,將 Log 以 pattern 解析生成新的 Label,並篩選 status 大於等於 200 與 小於 300 的 Log
{job="analyze"} | pattern "<_> - <_> <_> \"<method> <url> <protocol>\" <status> <_> <_> \"<_>\" <_>" | status >= 200 and status < 300

此例中,Pattern 使用空格或文字內容切割 Log,例如這邊使用 -",再利用 <> 包裹文字內容設定 Label Name,若有不需要設定 Label 的部分可以使用 _,解析時就會直接略過不生成 Label。以第一行 Log 為例,會生成以下的 Label:

  1. method: PUT
  2. url: /target/next-generation
  3. protocol: HTTP/2.0
  4. status: 404
# 篩選後的 Log 內容
16.97.233.22 - - [09/Jun/2022:14:13:44 -0700] "DELETE /extensible/functionalities HTTP/1.0" 200 27913

快還要更快

雖然 Loki 標榜查詢快速,但查詢速度仍與使用者的操作方式有關。就像是在資料庫中查詢資料時,菜鳥和資深工程師所寫的 SQL 效能可能就差了數百倍。在此篇文章 「5 tips for improving Grafana Loki query performance」 中,官方分享了五個提升 Loki 查詢效能的技巧,其中前四項可供初學 LogQL 者參考:

  1. Label or log stream selectors:在最初的選擇 Label 時,建議結合多個Label 進行篩選,如 {job="analyze", service="loki"},以此讓 Loki 能先篩選出較少的 Log,進而提升後續篩選效率,從而提升 Loki 效能。
  2. Line Filters:在篩選過程中,可使用多組 |~!~ 逐步減少 Log,取代使用正規表示式。例如,{job="analyze"} |= "/api/" |= "requests" 可先過濾出含 /api/ 的 Log,再過濾含 requests 的 Log。雖然可以使用正規表示式 |~ "/api/.*requests" 達到同樣效果,但純篩選的速度會更快。
  3. Time range selection:選擇較小的時間範圍也能顯著提升 Loki 效能,畢竟在實際使用情境下,使用者一般不需要一次查詢過長的時間範圍。
  4. Parsers:先進行文字內容篩選,再使用 Parser 解析 Log,如 {job="analyze"} |= "Logs" | logfmt,讓 Loki 首先篩選出含 Logs 的Log,再執行需較多計算的 logfmt Parser。

儲存

Loki 只對時間和 Label 建立 Index,而非像 Elasticsearch 對所有欄位建立 Index,這樣的設計讓它能夠使用較少的空間就可以儲存 Index。有良好的 Label 設計可以使 Loki 的 Index 非常小,例如:10TB 的 Log,其 Index 僅需要 20MB。同時 Loki 也會將剩餘內容進行壓縮,大幅減少儲存空間。

https://ithelp.ithome.com.tw/upload/images/20230929/20162175QshGiax49V.png
圖片來源 https://grafana.com/oss/loki/

Loki 會將收集和處理後的 Index 和 Log 儲存在 Object Storage,支援的 Object Storage 有:AWS S3Google Cloud StorageAzure Blob Storage 等。此外,也支援與 S3 兼容的 Object Storage 服務,例如 MinIO

Loki Docker Driver

Loki Docker Driver 是 Grafana Labs 官方提供的 Log 收集工具。它利用 Docker 的 Logging Driver 機制收集 Docker Container 的 Log,並將 Log 傳送到 Loki。可以將它看作是一個特化的 Promtail,因為它的設定方式與概念和 Promtail 幾乎是完全一樣的。Loki Docker Driver 會自動添加 compose_servicecontainer_namecompose_projectfilenamehostsource 這些 Label,再查詢時可以透過這些 Label 快速鎖定要查詢的 Log。對於剛接觸 Loki 的使用者來說,Loki Docker Driver 是一個很好的起點,無須了解與設定額外的 Agent,就可以輕鬆收集 Docker Container 的 Log。

與 Prometheus 的淵源

不知道你會不會懷疑為什麼 Loki 會借鑑那麼多 Prometheus 的設計?這主要是因為 Loki 是由 Tom WilkieDavid Kaltschmidt 所發起。如果有閱讀前面 Prometheus 長期儲存工具 Cortex 的章節,就會知道 Tom Wilkie 是早期在 Weaveworks 參與推動 Cortex 專案的工程師,而 David Kaltschmidt 同樣也在 Weaveworks 負責 Prometheus 和 Cortex 的相關工作。

Tom 與 David 兩人離開 Weaveworks 後,一同創立了 Kausal,旨在提供一套完整的 Observability Solution,協助開發者更深入地了解自己的程式運作。在他們的官網上,甚至可以看到一些與 Loki 查詢語法相似的介面。2018 年 5 月,Kausal 被 Grafana Labs 收購,根據 Introduction to Loki: Like Prometheus, but for Logs 這個影片中提到的 Loki Brief History,Loki 的啟動時間是在 2018/3/18,所以 Loki 一開始應該是作為 Kausal 產品的一部分,隨著收購後 Loki 才以獨立的產品正式亮相。

Lab

範例程式碼:14-loki

Quick Start

  1. 安裝 Loki Docker Driver

    docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
    
  2. 啟動所有服務

    docker-compose up -d
    
  3. 檢視服務

    1. FastAPI App: http://localhost:8000
      1. 對 FastAPI App 發送 HTTP Request,會生成 Log
        1. 透過瀏覽器發送 Request
          1. http://localhost:8000
          2. http://localhost:8000/io_task
          3. http://localhost:8000/cpu_task
          4. http://localhost:8000/random_sleep
          5. http://localhost:8000/random_status
          6. http://localhost:8000/error_test
    2. Nginx: http://localhost:8080
      1. 瀏覽 Nginx 時會生成 Access Log
    3. Grafana: http://localhost:3000,登入帳號密碼為 admin/admin
      1. 點擊左上 Menu > Explore,左上 Data Source 選擇 Loki,在 Label Filter 中 Label 選擇 compose_service,Value 選擇 fastapi,即可看到 fastapi Container 的 Log

      2. 瀏覽 http://localhost:8000/error_test,產生 Stack Trace 測試輸出多行 Log 後,查看 Grafana Explore 中的 fastapi Log 是否有收集到新的 Log 以及是否有正確合併多行 Log

      3. 若要生成更多 Log 也可以使用 k6 發送更多 Request

        k6 run --vus 1 --duration 300s k6-script.js
        
  4. 關閉所有服務

    docker-compose down
    

Goals

  1. 建立 Loki,接收 Loki Docker Driver 收集的 Container Log
  2. Loki Docker Driver
    1. 設定 loki-url,將 Loki Docker Driver 收集的 Log 傳送到 Loki
    2. 設定 loki-pipeline-stages,使用正規表示式將多行的 Log 合併成單行
  3. 建立 Grafana,讀取 Loki 的資料

小結

經過數年的發展,Loki 已逐漸成為一個成熟且穩定的 Log Aggregation System。其設計理念與 Prometheus 高度相似,習慣使用 Prometheus 的使用者能夠迅速且輕鬆地上手 Loki。此外,有許多 Log 收集工具,如 Fluent Bit 和 Vector,皆提供對 Loki 的支援,讓使用者可以根據自己的需求靈活選擇合適的工具。由於 Loki 的 Index 體積相對較小,再加上能夠利用 Object Storage 進行儲存,使得 Loki 在成本方面有著顯著的優勢,能讓使用者盡可能地保存 Log 資料。

RedHat 的 K8s 產品 OpenShift 也提供了 Loki Operator,方便使用者能夠更輕鬆地部署和使用 Loki,反映出 Loki 已被廣泛應用於各種系統中作為 Log Aggregation System。搭配 Grafana 良好的使用體驗,或許 Loki 也能成為你下一次建立 Log Aggregation System 時的首選。

參考資料

  1. Label best practices
  2. 5 tips for improving Grafana Loki query performance
  3. Getting started with Grafana Loki (Grafana Office Hours #09)
  4. Introduction to Loki: Like Prometheus, but for Logs

上一篇
Logs - 紀錄的一切都將成為呈堂證供
下一篇
Promtail — Loki 御用 Log 收集器
系列文
時光之鏡:透視過去、現在與未來的 Observability30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言